/*
 * Copyright (c) gitee.com/jiandong001.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import { NotificationOnlyHandler } from '../packagerConnection/NotificationOnlyHandler';
import { DevSupportManagerBase } from './DevSupportManagerBase';
import { ConnectionCallback } from '../packagerConnection/ReconnectingWebSocket';
import HashMap from '@ohos.util.HashMap';
import { RequestHandler } from '../packagerConnection/RequestHandler';
import prompt from '@ohos.prompt';
import fileio from '@ohos.fileio';
import hilog from '@ohos.hilog';
import http from '@ohos.net.http';

import { PackagerStatusCallback } from './PackagerStatusCallback';
import { BundleInfo } from './BundleDownloader';
import { ReactContext } from '../bridge/ReactContext';
import { DevInternalSettings } from './DevInternalSettings';
import { Assertions } from '../inner/Assertions';
import { DevBundleDownloadListener } from './DevBundleDownloadListener';
import { BundleStatusProvider } from './InspectorPackagerConnection';
import { BundleDownloader } from './BundleDownloader';
import { PackagerStatusCheck } from './PackagerStatusCheck';
import { JSPackagerClient } from '../packagerConnection/JSPackagerClient';

export interface OnServerContentChangeListener {
  onServerContentChanged(): void;
}

export abstract class PackagerCommandListener {
  abstract onPackagerConnected(): void

  abstract onPackagerDisconnected(): void

  abstract onPackagerReloadCommand(): void

  //  abstract onPackagerDevMenuCommand(): void

  //  onCaptureHeapCommand(final Responder responder): void

  // Allow apps to provide listeners for custom packager commands.
  //  @Nullable
  //  Map<String, RequestHandler> customCommandHandlers();
}

export interface PackagerCustomCommandProvider {}

//public interface SymbolicationListener {
//  void onSymbolicationComplete(@Nullable Iterable<StackFrame> stackFrames);
//}

class BundleType {
  public static readonly BUNDLE = "bundle";
  public static readonly MAP = "map";
  private readonly mTypeID: string;

  constructor(typeID: string) {
    this.mTypeID = typeID;
  }

  public typeID(): string {
    return this.mTypeID;
  }
}

export class DevServerHelper {
  public static readonly TAG = "React DevServerHelper"
  public static readonly RELOAD_APP_EXTRA_JS_PROXY = "jsproxy";
  private static readonly HTTP_CONNECT_TIMEOUT_MS = 60000; // 5000
  private static readonly HTTP_READ_TIMEOUT_MS = 60000;
  private static readonly HTTP_WRITE_TIMEOUT_MS = 60000;
  private static readonly DEBUGGER_MSG_DISABLE = "{ \"id\":1,\"method\":\"Debugger.disable\" }";
  private readonly mSettings: DevInternalSettings;
  //  private readonly mClient: OkHttpClient;
  private readonly mHttpRequest: http.HttpRequest
  private readonly mOptions: http.HttpRequestOptions = {
    method: http.RequestMethod.GET,
//    header: { 'Accept': 'multipart/mixed', 'Content-Type': 'application\/javascript' },
    header: { 'Content-Type': 'application\/javascript' },
    connectTimeout: 60000, // Optional. The default value is 60000, in ms.
    readTimeout: 60000, // Optional. The default value is 60000, in ms.
  }
  private readonly mBundleDownloader: BundleDownloader;
  private readonly mPackagerStatusCheck: PackagerStatusCheck;
  private readonly mPackageName: string;
  private mPackagerClient?: JSPackagerClient;
  //private @Nullable InspectorPackagerConnection mInspectorPackagerConnection;
  private mBundlerStatusProvider: BundleStatusProvider;

  public constructor(
    settings: DevInternalSettings,
    packageName: string,
    bundleStatusProvider: BundleStatusProvider) {
    this.mSettings = settings;
    this.mBundlerStatusProvider = bundleStatusProvider;
    this.mHttpRequest = http.createHttp()
    this.mBundleDownloader = new BundleDownloader();
    this.mPackagerStatusCheck = new PackagerStatusCheck();
    this.mPackageName = packageName;
  }

  public openPackagerConnection(clientId: string, commandListener: PackagerCommandListener): void {

    hilog.info(0x0000, DevServerHelper.TAG, '%{public}s', 'openPackagerConnection clientId: ' + clientId);

    if (this.mPackagerClient != null) {
      hilog.warn(0x0000, DevServerHelper.TAG, '%{public}s', 'Packager connection already open, nooping.');
      return;
    }

    // TODO： put it to promise or AsyncTask
    let handlers = new HashMap<string, RequestHandler>();
    handlers.set("reload", ((commandListener: PackagerCommandListener) => {
      return new class temp extends NotificationOnlyHandler {
        constructor(private mCommandListener: PackagerCommandListener) {
          super()
        }

        public onNotification(params?: Object): void {
          commandListener.onPackagerReloadCommand();
        }
      }(commandListener)
    })(commandListener));


    let onPackagerConnectedCallback: ConnectionCallback = ((commandListener: PackagerCommandListener) => {
      return new class temp extends ConnectionCallback {
        constructor(private mCommandListener: PackagerCommandListener) {
          super()
        }

        public onConnected(): void {
          this.mCommandListener.onPackagerConnected();
        }

        public onDisconnected(): void {
          this.mCommandListener.onPackagerDisconnected();
        }
      }(commandListener)
    })(commandListener);

    this.mPackagerClient = new JSPackagerClient(clientId, this.mSettings.getPackagerConnectionSettings(), handlers, onPackagerConnectedCallback);
    this.mPackagerClient.init();
  }

  public closePackagerConnection(): void {
    //    new AsyncTask<Void, Void, Void>() {
    ////      @Override
    //      protected Void doInBackground(Void... params) {
    //        if (mPackagerClient != null) {
    //          mPackagerClient.close();
    //          mPackagerClient = null;
    //        }
    //return null;
    //}
    //}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  }

  //public void openInspectorConnection() {
  //    if (mInspectorPackagerConnection != null) {
  //      FLog.w(ReactConstants.TAG, "Inspector connection already open, nooping.");
  //      return;
  //    }
  //    new AsyncTask<Void, Void, Void>() {
  //      @Override
  //      protected Void doInBackground(Void... params) {
  //        mInspectorPackagerConnection =
  //            new InspectorPackagerConnection(
  //                getInspectorDeviceUrl(), mPackageName, mBundlerStatusProvider);
  //        mInspectorPackagerConnection.connect();
  //        return null;
  //      }
  //}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  //}
  //
  public disableDebugger(): void {
    //  if (mInspectorPackagerConnection != null) {
    //    mInspectorPackagerConnection.sendEventToAllConnections(DEBUGGER_MSG_DISABLE);
    //  }
  }
  //
  //public void closeInspectorConnection() {
  //    new AsyncTask<Void, Void, Void>() {
  //      @Override
  //      protected Void doInBackground(Void... params) {
  //        if (mInspectorPackagerConnection != null) {
  //          mInspectorPackagerConnection.closeQuietly();
  //          mInspectorPackagerConnection = null;
  //        }
  //return null;
  //}
  //}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  //}

  public openUrl(context: ReactContext, url: string, errorMessage: string): void {
    //    new AsyncTask<Void, String, Boolean>() {
    ////      @Override
    //      protected Boolean doInBackground(Void... ignore) {
    //        return doSync();
    //      }
    //
    //public doSync(): boolean {
    //        try {
    //          let openUrlEndpoint: string = getOpenUrlEndpoint(context);
    //          let jsonString: string = new JSONObject().put("url", url).toString();
    //          let body: RequestBody = RequestBody.create(MediaType.parse("application/json"), jsonString);
    //
    //          let request: Request = new Request.Builder().url(openUrlEndpoint).post(body).build();
    //          let client: OkHttpClient = new OkHttpClient();
    //          client.newCall(request).execute();
    //          return true;
    //        } catch (e) { //JSONException | IOException
    //          hilog.error(0x0000, DevServerHelper.TAG, 'Failed to open URL %{public}s', url + (e as Error).message);
    //          return false;
    //        }
    //}
    //
    ////@Override
    ////protected void onPostExecute(Boolean result) {
    ////  if (!result) {
    ////    RNLog.w(context, errorMessage);
    ////  }
    ////}
    //}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  }

  //public void symbolicateStackTrace(
  //Iterable<StackFrame> stackFrames, final SymbolicationListener listener) {
  //    try {
  //      final String symbolicateURL =
  //          createSymbolicateURL(mSettings.getPackagerConnectionSettings().getDebugServerHost());
  //      final JSONArray jsonStackFrames = new JSONArray();
  //      for (final StackFrame stackFrame : stackFrames) {
  //        jsonStackFrames.put(stackFrame.toJSON());
  //      }
  //      final Request request =
  //          new Request.Builder()
  //              .url(symbolicateURL)
  //              .post(
  //                  RequestBody.create(
  //                      MediaType.parse("application/json"),
  //                      new JSONObject().put("stack", jsonStackFrames).toString()))
  //              .build();
  //      Call symbolicateCall = Assertions.assertNotNull(mClient.newCall(request));
  //      symbolicateCall.enqueue(
  //          new Callback() {
  //            @Override
  //            public void onFailure(Call call, IOException e) {
  //              FLog.w(
  //                  ReactConstants.TAG,
  //                  "Got IOException when attempting symbolicate stack trace: " + e.getMessage());
  //              listener.onSymbolicationComplete(null);
  //            }
  //
  //            @Override
  //            public void onResponse(Call call, final Response response) throws IOException {
  //              try {
  //                listener.onSymbolicationComplete(
  //                    Arrays.asList(
  //                        StackTraceHelper.convertJsStackTrace(
  //                            new JSONObject(response.body().string()).getJSONArray("stack"))));
  //              } catch (JSONException exception) {
  //                listener.onSymbolicationComplete(null);
  //              }
  //            }
  //          });
  //    } catch (JSONException e) {
  //  FLog.w(
  //  ReactConstants.TAG,
  //    "Got JSONException when attempting symbolicate stack trace: " + e.getMessage());
  //}
  //}
  //
  //public void openStackFrameCall(StackFrame stackFrame) {
  //    final String openStackFrameURL =
  //        createOpenStackFrameURL(mSettings.getPackagerConnectionSettings().getDebugServerHost());
  //    final Request request =
  //        new Request.Builder()
  //            .url(openStackFrameURL)
  //            .post(
  //                RequestBody.create(
  //                    MediaType.parse("application/json"), stackFrame.toJSON().toString()))
  //            .build();
  //    Call symbolicateCall = Assertions.assertNotNull(mClient.newCall(request));
  //    symbolicateCall.enqueue(
  //        new Callback() {
  //          @Override
  //          public void onFailure(Call call, IOException e) {
  //            FLog.w(
  //                ReactConstants.TAG,
  //                "Got IOException when attempting to open stack frame: " + e.getMessage());
  //          }
  //
  //          @Override
  //          public void onResponse(Call call, final Response response) throws IOException {
  //            // We don't have a listener for this.
  //          }
  //        });
  //  }

  public getWebsocketProxyURL(): string {
    return `ws://${this.mSettings.getPackagerConnectionSettings().getDebugServerHost()}/debugger-proxy?role=client`

    //  return String.format(
    //  Locale.US,
    //    "ws://%s/debugger-proxy?role=client",
    //  this.mSettings.getPackagerConnectionSettings().getDebugServerHost());
  }

  //private String getInspectorDeviceUrl() {
  //  return String.format(
  //  Locale.US,
  //    "http://%s/inspector/device?name=%s&app=%s",
  //  mSettings.getPackagerConnectionSettings().getInspectorServerHost(),
  //  AndroidInfoHelpers.getFriendlyDeviceName(),
  //    mPackageName);
  //}

  //public downloadBundleFromURL(
  //callback: DevBundleDownloadListener,
  //outputFile: File,
  //bundleURL: string,
  //bundleInfo: BundleInfo): void {
  //  this.mBundleDownloader.downloadBundleFromURL(callback, outputFile, bundleURL, bundleInfo);
  //}

  //private getOpenUrlEndpoint(context: Context):string {
  //  return String.format(
  //  Locale.US, "http://%s/open-url", AndroidInfoHelpers.getServerHost(context));
  //}

  public downloadBundleFromURL(
    callback: DevBundleDownloadListener,
    outputFile: string, //File,
    bundleURL: string,
    bundleInfo: BundleInfo,
    request: http.HttpRequest): void {
    //    requestBuilder?: Request.Builder): void {
    this.mBundleDownloader.downloadBundleFromURL(
      callback, outputFile, bundleURL, this.mOptions, bundleInfo);
  }

  /** @return the host to use when connecting to the bundle server from the host itself. */
  private getHostForJSProxy(): string {
    // Use custom port if configured. Note that host stays "localhost".
    let host = this.mSettings.getPackagerConnectionSettings().getDebugServerHost()
    // Assertions.assertNotNull(this.mSettings.getPackagerConnectionSettings().getDebugServerHost() != null);
    let portOffset: number = host.lastIndexOf(':');
    if (portOffset > -1) {
      return "localhost" + host.substring(portOffset);
    } else {
      //return AndroidInfoHelpers.DEVICE_LOCALHOST;
      return "127.0.0.1"
    }
  }

  /** @return whether we should enable dev mode when requesting JS bundles. */
  private getDevMode(): boolean {
    return this.mSettings.isJSDevModeEnabled();
  }

  /** @return whether we should request minified JS bundles. */
  //private boolean getJSMinifyMode() {
  //  return mSettings.isJSMinifyEnabled();
  //}

  //private  createBundleURL( mainModuleID: string,  type: BundleType, host: string): string {
  //  return createBundleURL(mainModuleID, type, host, false, true);
  //}

  private createSplitBundleURL(mainModuleID: string, host: string): string {
    return this.createBundleURL(mainModuleID, new BundleType(BundleType.BUNDLE), host, true, false);
  }

  private createBundleURL(mainModuleID: string, type: BundleType, host: string = '', modulesOnly: boolean = false, runModule: boolean = true): string {

    let runtimeBytecodeVersion = 100

    //        ReactBuildConfig.HERMES_BYTECODE_VERSION != 0
    //            ? "&runtimeBytecodeVersion=" + ReactBuildConfig.HERMES_BYTECODE_VERSION
    //            : "";
    //    return String.format(
    //        Locale.US,
    //        "http://%s/%s.%s?platform=android&dev=%s&minify=%s&app=%s&modulesOnly=%s&runModule=%s%s",
    //        host??this.mSettings.getPackagerConnectionSettings().getDebugServerHost(),
    //        mainModuleID,
    //        type.typeID(),
    //        this.getDevMode(),
    ////        this.getJSMinifyMode(),
    //        this.mPackageName,
    //        modulesOnly ? "true" : "false",
    //        runModule ? "true" : "false",
    //        runtimeBytecodeVersion);

    return `http:\/\/${host == '' ? this.mSettings.getPackagerConnectionSettings().getDebugServerHost() : host}\/${mainModuleID}.${type.typeID()}?platform=android&dev=${this.getDevMode()}&minify=false&app=${this.mPackageName}&modulesOnly=${modulesOnly}&runModule=${runModule}&runtimeBytecodeVersion=${runtimeBytecodeVersion}`

  }

  //private createBundleURL(mainModuleID:string, type: BundleType): String {
  //  return this.createBundleURL(
  //    mainModuleID, type, mSettings.getPackagerConnectionSettings().getDebugServerHost());
  //}

  private static createResourceURL(host: string, resourcePath: string): string {
    return `http:\/\/${host}\/${resourcePath}`
    //  return String.format(Locale.US, "http://%s/%s", host, resourcePath);
  }

  private static createSymbolicateURL(host: string): string {
    return `http:\/\/${host}\/symbolicate`
    //  return String.format(Locale.US, "http://%s/symbolicate", host);
  }

  private static createOpenStackFrameURL(host): string {
    return `http:\/\/${host}/open-stack-frame`
    //  return String.format(Locale.US, "http://%s/open-stack-frame", host);

  }

  public getDevServerBundleURL(jsModulePath: string): string {
    return this.createBundleURL(
      jsModulePath,
      new BundleType(BundleType.BUNDLE),
    this.mSettings.getPackagerConnectionSettings().getDebugServerHost());
  }

  public getDevServerSplitBundleURL(jsModulePath: string): string {
    return this.createSplitBundleURL(
      jsModulePath, this.mSettings.getPackagerConnectionSettings().getDebugServerHost());
  }

  public isPackagerRunning(callback: PackagerStatusCallback): void {
    let host: string = this.mSettings.getPackagerConnectionSettings().getDebugServerHost();
    if (host == null) {
      hilog.warn(0x0, DevServerHelper.TAG, "No packager host configured.");
      callback.onPackagerStatusFetched(false);
    } else {
      this.mPackagerStatusCheck.run(host, callback);
    }
  }

  private createLaunchJSDevtoolsCommandUrl(): string {
    return `http:\/\/${this.mSettings.getPackagerConnectionSettings().getDebugServerHost()}\/launch-js-devtools`
    //  return String.format(
    //  Locale.US,
    //    "http://%s/launch-js-devtools",
    //  this.mSettings.getPackagerConnectionSettings().getDebugServerHost());
  }

  public launchJSDevtools(): void {

    let httpRequest = http.createHttp();
    // Subscribe to the HTTP response header, which is returned earlier than httpRequest. Whether to subscribe to the HTTP response header is up to your decision.
    // on('headerReceive', AsyncCallback) is replaced by on('headersReceive', Callback) since API version 8.
    httpRequest.on('headersReceive', (header) => {
      console.info('header: ' + JSON.stringify(header));
    });
    httpRequest.request(this.createLaunchJSDevtoolsCommandUrl(), this.mOptions, (err, data) => {
      if (!err) {
        // ignore HTTP call response, this is just to open a debugger page and there is no reason
        // to report failures from here

        console.info('Result:' + data.result);
        console.info('code:' + data.responseCode);
        // data.header contains the HTTP response header. Parse the content based on service requirements.
        console.info('header:' + JSON.stringify(data.header));
        console.info('cookies:' + data.cookies); // 8+
      } else {
        // ignore HTTP call response - see above
        console.info('error:' + JSON.stringify(err));
        // Call the destroy() method to release resources after HttpRequest is complete.
        httpRequest.destroy();
      }
    }
    );
  }

  public getSourceMapUrl(mainModuleName: string): string {
    return this.createBundleURL(mainModuleName, new BundleType(BundleType.MAP));
  }

  public getSourceUrl(mainModuleName: string): string {
    return this.createBundleURL(mainModuleName, new BundleType(BundleType.BUNDLE));
  }

  public getJSBundleURLForRemoteDebugging(mainModuleName: string): string {
    // The host we use when connecting to the JS bundle server from the emulator is not the
    // same as the one needed to connect to the same server from the JavaScript proxy running on the
    // host itself.
    return this.createBundleURL(mainModuleName, new BundleType(BundleType.BUNDLE), this.getHostForJSProxy());
  }

  /**
   * This is a debug-only utility to allow fetching a file via packager. It's made synchronous for
   * simplicity, but should only be used if it's absolutely necessary.
   *
   * @return the file with the fetched content, or null if there's any failure.
   */
  public downloadBundleResourceFromUrlSync(resourcePath: string, outputFile: string /*File*/
  ): string | null { /*File*/
    let resourceURL: string = DevServerHelper.createResourceURL(
    this.mSettings.getPackagerConnectionSettings().getDebugServerHost(), resourcePath);

    let httpRequest = http.createHttp();
    httpRequest.request(resourceURL, this.mOptions, (err, data) => {
      if (!err) {
        console.info('Result:' + data.result);
        console.info('code:' + data.responseCode);
        console.info('header:' + JSON.stringify(data.header));
        console.info('cookies:' + data.cookies); // 8+

        let code: number = data.responseCode as number
        if (code < 200 || code > 299) return null;

        let bufferData = data.result as string
        let writeFd = fileio.openSync(outputFile, 0o2 | 0o100, 0o666)
        let stream = fileio.fdopenStreamSync(writeFd, "r+")
        let writelength = stream.writeSync(bufferData, {
          offset: 0,
          length: bufferData.length,
          position: 0,
          encoding: 'utf-8'
        })
        prompt.showToast({ message: 'write_fileIo success =>' + bufferData, duration: 2000 })
        console.log('FileIo WriteLength = ' + writelength)
        stream.close()
        fileio.close(writeFd)
        return outputFile
      } else {
        console.info('error:' + JSON.stringify(err));
        httpRequest.destroy();
        return null
      }
    });
    return outputFile
  }
}
